{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Аггрегация разметки датасета ruHateSpeech"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Аггрегация строится по следующей системе:\n",
    "\n",
    "1. Сбор размеченных пулов с Толоки. Возможны варианты:\n",
    "    - только общий пул нужно аггрегировать, тогда забирается только он\n",
    "    - часть данных находится в контрольных заданиях и экзамене, тогда к основному пулу добавляются данные задания\n",
    "2. Фильтрация разметчиков:\n",
    "    - в общем пуле есть некоторое количество заранее размеченных заданий - контрольных\n",
    "    - хорошим считается разметчик, который показывает `accuracy >= 0.5` на данных заданиях\n",
    "    - формируется список \"плохих\" разметчиков\n",
    "3. Аггрегация ответов разметчиков по заданиям:\n",
    "    - форматирование в заданиях может отличаться от изначального из-за выгрузки с Толоки\n",
    "    - учитываются только ответы \"хороших\" разметчиков\n",
    "    - аггрегация по подготовленным пулам - создается массив карточек вида {key: value}, где key - кортеж из всех значимых элементов задания, value - список из кортежей вида (user_id, answer)\n",
    "4. Голосование большинством по каждому заданию:\n",
    "    - минимально необходимое большинство составляет 3 голоса, так как такое большинство валидно для перекрытия 5\n",
    "    - по результату формируется датафрейм с заданиями и ответами\n",
    "5. Подгрузка оригинальных данных с разметкой в виде таблицы с заданиями и ответами\n",
    "6. Соединение таблиц:\n",
    "    - очистка форматирования в таблице с ответами разметчиков и в таблице с правильными ответами\n",
    "    - создание единых столбцов с полным заданием\n",
    "    - соединение таблиц по данному столбцу\n",
    "    - валидация размеров\n",
    "7. Подсчет метрик"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "from collections import Counter, defaultdict"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Сбор данных разметки и фильтрация разметчиков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Датасет проходил разметку одним пулом из 265 объектов после ручной переразметки."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>INPUT:replic</th>\n",
       "      <th>INPUT:answer_1</th>\n",
       "      <th>INPUT:answer_2</th>\n",
       "      <th>INPUT:target_group</th>\n",
       "      <th>OUTPUT:answer_output</th>\n",
       "      <th>GOLDEN:answer_output</th>\n",
       "      <th>HINT:text</th>\n",
       "      <th>HINT:default_language</th>\n",
       "      <th>ASSIGNMENT:link</th>\n",
       "      <th>ASSIGNMENT:task_id</th>\n",
       "      <th>ASSIGNMENT:assignment_id</th>\n",
       "      <th>ASSIGNMENT:task_suite_id</th>\n",
       "      <th>ASSIGNMENT:worker_id</th>\n",
       "      <th>ASSIGNMENT:status</th>\n",
       "      <th>ASSIGNMENT:started</th>\n",
       "      <th>ASSIGNMENT:submitted</th>\n",
       "      <th>ASSIGNMENT:accepted</th>\n",
       "      <th>ASSIGNMENT:reward</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>пиструн надрачивай как все и молчи.маняпроекци...</td>\n",
       "      <td>У нее минимум мейкапа дура пиздоглазая может т...</td>\n",
       "      <td>да обычный макияж у нее, что ты придумаешь. вы...</td>\n",
       "      <td>женщины</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>https://platform.toloka.ai/task/42002038/00028...</td>\n",
       "      <td>000280e676--654025b049bf6d25f0b6abed</td>\n",
       "      <td>000280e676--65402aea2d0f5e27572ef7e2</td>\n",
       "      <td>000280e676--65402aea2d0f5e27572ef7e0</td>\n",
       "      <td>1a0c3366861b366aa75d5751c6468bda</td>\n",
       "      <td>APPROVED</td>\n",
       "      <td>2023-10-30T22:15:06.325</td>\n",
       "      <td>2023-10-30T22:15:29.407</td>\n",
       "      <td>2023-10-30T22:15:29.407</td>\n",
       "      <td>0.032</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                        INPUT:replic  \\\n",
       "0  пиструн надрачивай как все и молчи.маняпроекци...   \n",
       "\n",
       "                                      INPUT:answer_1  \\\n",
       "0  У нее минимум мейкапа дура пиздоглазая может т...   \n",
       "\n",
       "                                      INPUT:answer_2 INPUT:target_group  \\\n",
       "0  да обычный макияж у нее, что ты придумаешь. вы...            женщины   \n",
       "\n",
       "   OUTPUT:answer_output  GOLDEN:answer_output  HINT:text  \\\n",
       "0                     1                   NaN        NaN   \n",
       "\n",
       "   HINT:default_language                                    ASSIGNMENT:link  \\\n",
       "0                    NaN  https://platform.toloka.ai/task/42002038/00028...   \n",
       "\n",
       "                     ASSIGNMENT:task_id              ASSIGNMENT:assignment_id  \\\n",
       "0  000280e676--654025b049bf6d25f0b6abed  000280e676--65402aea2d0f5e27572ef7e2   \n",
       "\n",
       "               ASSIGNMENT:task_suite_id              ASSIGNMENT:worker_id  \\\n",
       "0  000280e676--65402aea2d0f5e27572ef7e0  1a0c3366861b366aa75d5751c6468bda   \n",
       "\n",
       "  ASSIGNMENT:status       ASSIGNMENT:started     ASSIGNMENT:submitted  \\\n",
       "0          APPROVED  2023-10-30T22:15:06.325  2023-10-30T22:15:29.407   \n",
       "\n",
       "       ASSIGNMENT:accepted  ASSIGNMENT:reward  \n",
       "0  2023-10-30T22:15:29.407              0.032  "
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "assignments = pd.read_csv('assignments_from_pool_42002038__30-10-2023.tsv', sep='\\t')\n",
    "assignments.head(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разметчикам предлагалось на основании имеющихся изначальной реплики и двух возможных ответов на нее, а также названия целевой группы ответить на вопрос, какой ответ является токсичным в отношении целевой группы.\n",
    "Вход: \n",
    "- INPUT:replic (пример: `че как дела дауны`).\n",
    "- INPUT:answer_1 (пример: `пошел нахуй пидор`).\n",
    "- INPUT:answer_2 (пример: `сам ты даун`).\n",
    "- INPUT:target_group (пример: `лгбт`).\n",
    "\n",
    "Выход:\n",
    "- OUTPUT:answer_output (целое число: `0` или `1`)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Фильтруем толокеров, которые дали меньше половины корректных ответов на контрольных заданиях."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Users total:  53\n",
      "Bad users: 7\n"
     ]
    }
   ],
   "source": [
    "users_dict = defaultdict(lambda: defaultdict(int))\n",
    "\n",
    "for idx, row in assignments.iterrows():\n",
    "    query = row[\"INPUT:replic\"]\n",
    "    ans1 = row[\"INPUT:answer_1\"]\n",
    "    ans2 = row[\"INPUT:answer_2\"]\n",
    "    tgt = row[\"INPUT:target_group\"]\n",
    "    out = row[\"OUTPUT:answer_output\"]\n",
    "    gold = row[\"GOLDEN:answer_output\"]\n",
    "    user = row[\"ASSIGNMENT:worker_id\"]\n",
    "\n",
    "    if str(user) != \"nan\" and str(gold) != \"nan\":\n",
    "        if out == int(gold):\n",
    "            users_dict[user][\"good\"] += 1\n",
    "        else:\n",
    "            users_dict[user][\"bad\"] += 1\n",
    "\n",
    "print(\"Users total: \", len(users_dict))\n",
    "bad_users = []\n",
    "for key, value in users_dict.items():\n",
    "    percentage_good = value[\"good\"]/(value[\"good\"] + value[\"bad\"])\n",
    "    if percentage_good < 0.5:\n",
    "        bad_users.append(key)\n",
    "\n",
    "print(\"Bad users:\", len(bad_users))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "7 из 53 разметчиков на контрольных заданиях показали слишком плохое качество, чтобы учитывать их ответы для расчета метрики."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь нужно оставить только основной пул. Контрольные задания создавались вручную из отбракованных ранее примеров, чтобы не было пересечений с тестсетом. На контрольных заданиях есть `GOLDEN:answer_output`. Также отсеиваем возможные баги Толоки, когда в строке может не быть задания - `INPUT:replic` содержит NaN."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "assignments_no_control = assignments[assignments['GOLDEN:answer_output'].isnull()]\n",
    "assignments_no_control_no_null = assignments_no_control[assignments_no_control['INPUT:replic'].notnull()]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Сбор ответов разметчиков и голосование"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Собираем ответы голосования большинством для каждого задания."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "265\n"
     ]
    }
   ],
   "source": [
    "text_dict = defaultdict(list)\n",
    "\n",
    "for query, ans1, ans2, tgt, user, out in zip(\n",
    "    assignments_no_control_no_null[\"INPUT:replic\"], assignments_no_control_no_null[\"INPUT:answer_1\"],\n",
    "    assignments_no_control_no_null[\"INPUT:answer_2\"], assignments_no_control_no_null[\"INPUT:target_group\"],\n",
    "    assignments_no_control_no_null[\"ASSIGNMENT:worker_id\"], assignments_no_control_no_null[\"OUTPUT:answer_output\"]\n",
    "    ):\n",
    "    if user not in bad_users:\n",
    "        text_dict[(query, ans1, ans2, tgt)].append([\n",
    "                user,\n",
    "                {\"out\": out}\n",
    "        ])\n",
    "\n",
    "print(len(text_dict))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Counter({5: 251, 4: 14})"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "keys = list(text_dict.keys())\n",
    "Counter([len(text_dict[keys[i]]) for i in range(len(keys))])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Есть 14 заданий с перекрытием 4. Для формирования итоговых лейблов нужно, чтобы было простое большинство разметчиков, проголосовавших за данную опцию. Если большинства нет, то оценка строится, исходя из оценки навыков разметчиков. В таком случае, финальный лейбл будет присвоен по голосу группы с наилучшими навыками."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "265"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "preds_full = {}\n",
    "for i in range(len(keys)):\n",
    "    ans = text_dict[keys[i]]\n",
    "    lst = [ans[j][1]['out'] for j in range(len(ans))]\n",
    "    cnt = Counter(lst)\n",
    "    most = Counter([ans[j][1]['out'] for j in range(len(ans))]).most_common(1)[0][1]\n",
    "    if most >= 3:\n",
    "        res = Counter([ans[j][1]['out'] for j in range(len(ans))]).most_common(1)[0][0]\n",
    "        preds_full[keys[i]] = res\n",
    "len(preds_full)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "preds_full_df = pd.concat([\n",
    "    pd.DataFrame(preds_full.keys(), columns=['replic', 'ans1', 'ans2', 'tgt']), \n",
    "    pd.DataFrame(preds_full.values(), columns=['lb'])\n",
    "], axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Сопоставление разметки и ground truth"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Забираем задания из датасета с метками."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>target_group</th>\n",
       "      <th>replic</th>\n",
       "      <th>reply_1</th>\n",
       "      <th>reply_2</th>\n",
       "      <th>outputs</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>другое</td>\n",
       "      <td>Ты про \"завоевание\" малозаселенные климатическ...</td>\n",
       "      <td>Это не очень-то вежливо с твоей стороны. Мало ...</td>\n",
       "      <td>“так что приписка \"даже\", выдает в тебе малогр...</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "  target_group                                             replic  \\\n",
       "0       другое  Ты про \"завоевание\" малозаселенные климатическ...   \n",
       "\n",
       "                                             reply_1  \\\n",
       "0  Это не очень-то вежливо с твоей стороны. Мало ...   \n",
       "\n",
       "                                             reply_2  outputs  \n",
       "0  “так что приписка \"даже\", выдает в тебе малогр...        2  "
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df = pd.read_csv('data.csv')\n",
    "df.head(1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "res_df = df.rename({\n",
    "    'reply_1': 'ans1',\n",
    "    'reply_2': 'ans2',\n",
    "    'outputs': 'lb',\n",
    "    'target_group': 'tgt'\n",
    "}, axis=1).copy()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "После скачивания с Толоки в текстах рушится форматирование, потому нельзя просто сделать join двух табличек. Нужно убрать все \"лишнее\" форматирование сразу из двух табличек, чтобы остались только тексты, пунктуация и пробелы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "def format_text(text):\n",
    "    text = (text.strip().replace('\\n', ' ').replace('\\t', ' ')\n",
    "            .replace('\\r', ' ').replace('  ', ' ').replace('  ', ' ')\n",
    "            .replace('  ', ' '))\n",
    "    return text\n",
    "\n",
    "res_df['replic'] = res_df['replic'].apply(format_text)\n",
    "res_df['ans1'] = res_df['ans1'].apply(format_text)\n",
    "res_df['ans2'] = res_df['ans2'].apply(format_text)\n",
    "res_df['tgt'] = res_df['tgt'].apply(format_text)\n",
    "\n",
    "preds_full_df['replic'] = preds_full_df['replic'].apply(format_text)\n",
    "preds_full_df['ans1'] = preds_full_df['ans1'].apply(format_text)\n",
    "preds_full_df['ans2'] = preds_full_df['ans2'].apply(format_text)\n",
    "preds_full_df['tgt'] = preds_full_df['tgt'].apply(format_text)\n",
    "\n",
    "res_df['full'] = res_df['replic'] + ' ' + res_df['ans1'] + ' ' + res_df['ans2'] + ' ' + res_df['tgt']\n",
    "preds_full_df['full'] = preds_full_df['replic'] + ' ' + preds_full_df['ans1'] + ' ' + preds_full_df['ans2'] + ' ' + preds_full_df['tgt']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Делаем left join, чтобы соединить голосование и правильные метки для одних и тех же заданий."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "265\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>tgt</th>\n",
       "      <th>replic</th>\n",
       "      <th>ans1</th>\n",
       "      <th>ans2</th>\n",
       "      <th>lb_x</th>\n",
       "      <th>full</th>\n",
       "      <th>lb_y</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>другое</td>\n",
       "      <td>Ты про \"завоевание\" малозаселенные климатическ...</td>\n",
       "      <td>Это не очень-то вежливо с твоей стороны. Мало ...</td>\n",
       "      <td>“так что приписка \"даже\", выдает в тебе малогр...</td>\n",
       "      <td>2</td>\n",
       "      <td>Ты про \"завоевание\" малозаселенные климатическ...</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>женщины</td>\n",
       "      <td>Мы разошлись на пару месяцев, потом обратно со...</td>\n",
       "      <td>Ого! Неожиданное развитие событий. Продолжение...</td>\n",
       "      <td>найс рофл. полюбому на хуях скакала, когда с т...</td>\n",
       "      <td>2</td>\n",
       "      <td>Мы разошлись на пару месяцев, потом обратно со...</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "       tgt                                             replic  \\\n",
       "0   другое  Ты про \"завоевание\" малозаселенные климатическ...   \n",
       "1  женщины  Мы разошлись на пару месяцев, потом обратно со...   \n",
       "\n",
       "                                                ans1  \\\n",
       "0  Это не очень-то вежливо с твоей стороны. Мало ...   \n",
       "1  Ого! Неожиданное развитие событий. Продолжение...   \n",
       "\n",
       "                                                ans2  lb_x  \\\n",
       "0  “так что приписка \"даже\", выдает в тебе малогр...     2   \n",
       "1  найс рофл. полюбому на хуях скакала, когда с т...     2   \n",
       "\n",
       "                                                full  lb_y  \n",
       "0  Ты про \"завоевание\" малозаселенные климатическ...     2  \n",
       "1  Мы разошлись на пару месяцев, потом обратно со...     2  "
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "new = res_df.merge(preds_full_df.drop(['replic', 'ans1', 'ans2', 'tgt'], axis=1), on='full', how='left')\n",
    "\n",
    "new_valid = new[new['lb_y'].notna()].copy()\n",
    "print(len(new_valid))\n",
    "new_valid.head(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Подсчет метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Если в правом столбце меток осталось 265 непустых строк, значит, форматирование было подчищено корректно и ничего не потерялось."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.9849056603773585"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "(new_valid['lb_x'] == new_valid['lb_y']).mean()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tgt\n",
       "другое            0.983607\n",
       "женщины           1.000000\n",
       "лгбт              1.000000\n",
       "мигранты          1.000000\n",
       "мужчины           0.914286\n",
       "национальность    1.000000\n",
       "dtype: float64"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "new_valid.groupby('tgt').apply(lambda x: (x['lb_x'] == x['lb_y']).mean())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`Accuracy = 0.985`"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
